📚Introduction to Python Programming
Welcome to this comprehensive Python Object-Oriented Programming tutorial! This guide will take you through the fundamentals of Python and dive deep into OOP concepts that will transform you into a proficient Python developer.
Python has become one of the most popular programming languages in the world, powering everything from web applications to artificial intelligence, data science, automation, and more. Understanding Object-Oriented Programming is crucial for writing clean, maintainable, and scalable code.
🐍What is Python?
Python is a high-level, interpreted, general-purpose programming language created by Guido van Rossum and first released in 1991. It emphasizes code readability with its notable use of significant indentation.
Key Features of Python:
- Easy to Learn and Use: Simple, clean syntax that resembles natural language
- Interpreted Language: Code is executed line by line, making debugging easier
- Dynamically Typed: No need to declare variable types explicitly
- Cross-Platform: Runs on Windows, macOS, Linux, and more
- Extensive Standard Library: Comes with modules for various tasks
- Multi-Paradigm: Supports procedural, object-oriented, and functional programming
- Large Community: Vast ecosystem of libraries and frameworks
- Free and Open Source: Available for everyone to use and contribute
Python Applications:
- Web Development: Django, Flask, FastAPI
- Data Science & Analytics: NumPy, Pandas, Matplotlib
- Machine Learning & AI: TensorFlow, PyTorch, scikit-learn
- Automation & Scripting: Task automation, web scraping
- Game Development: Pygame
- Desktop Applications: Tkinter, PyQt
- Scientific Computing: SciPy, SymPy
# Simple Python Hello World Program
print("Hello, World!")
# Variables - No type declaration needed
name = "Python"
version = 3.12
is_popular = True
print(f"Welcome to {name} {version}!")
Welcome to Python 3.12!
🎯Object-Oriented Programming Basics
Object-Oriented Programming (OOP) is a programming paradigm that organizes code around objects rather than functions and logic. An object is a data structure that contains data (attributes) and code (methods).
Core OOP Concepts:
- Class: A blueprint or template for creating objects
- Object: An instance of a class with specific data
- Attributes: Variables that belong to a class/object
- Methods: Functions that belong to a class/object
- Self: Reference to the current instance of the class
Simple Class Example:
# Defining a simple class
class Dog:
# Constructor method - initializes object
def __init__(self, name, breed):
self.name = name # Instance attribute
self.breed = breed # Instance attribute
# Instance method
def bark(self):
return f"{self.name} says Woof!"
# Another instance method
def info(self):
return f"{self.name} is a {self.breed}"
# Creating objects (instances) of the Dog class
dog1 = Dog("Buddy", "Golden Retriever")
dog2 = Dog("Max", "German Shepherd")
# Calling methods on objects
print(dog1.bark())
print(dog2.info())
Max is a German Shepherd
💡Why Use Object-Oriented Programming?
Object-Oriented Programming offers numerous advantages that make it the preferred paradigm for large-scale software development.
Benefits of OOP:
- Modularity: Code is organized into self-contained objects, making it easier to understand and maintain
- Reusability: Objects and classes can be reused across different programs, reducing redundancy
- Scalability: Easy to add new features without modifying existing code
- Maintainability: Changes can be made in one place without affecting other parts
- Security: Data hiding through encapsulation protects data from unauthorized access
- Real-World Modeling: Maps naturally to real-world entities and relationships
- Flexibility: Polymorphism allows different objects to respond to the same method call differently
- Code Organization: Logical structure makes code easier to navigate and understand
Four Pillars of OOP:
1. Encapsulation
Bundling data and methods together, hiding internal details
2. Abstraction
Hiding complexity, showing only essential features
3. Inheritance
Creating new classes from existing ones, promoting reuse
4. Polymorphism
Same interface for different data types and objects
OOP vs Procedural Programming:
# Procedural Approach
def calculate_area(length, width):
return length * width
area = calculate_area(10, 5)
# Object-Oriented Approach
class Rectangle:
def __init__(self, length, width):
self.length = length
self.width = width
def area(self):
return self.length * self.width
rect = Rectangle(10, 5)
area = rect.area()
print(f"Area: {area}")
1Encapsulation
Encapsulation is the bundling of data and methods that operate on that data within a single unit (class), restricting direct access to some components.
Key Points:
- Protects internal state from unauthorized access
- Reduces system complexity by hiding implementation details
- Increases flexibility and maintainability of code
- Achieved using public, protected, and private access modifiers
- Follows the principle of data hiding
class BankAccount:
def __init__(self, balance):
self.__balance = balance # Private attribute
def deposit(self, amount):
if amount > 0:
self.__balance += amount
return f"Deposited ${amount}"
def get_balance(self):
return self.__balance
account = BankAccount(1000)
print(account.deposit(500))
print(account.get_balance())
1500
2Abstraction
Abstraction hides complex implementation details and shows only the necessary features of an object.
Key Points:
- Focuses on what an object does rather than how it does it
- Reduces programming complexity and effort
- Implemented using abstract classes and interfaces
- Separates interface from implementation
- Enables code reusability and modularity
- Uses ABC (Abstract Base Class) module in Python
from abc import ABC, abstractmethod
class Vehicle(ABC):
@abstractmethod
def start(self):
pass
@abstractmethod
def stop(self):
pass
class Car(Vehicle):
def start(self):
return "Car engine started"
def stop(self):
return "Car engine stopped"
car = Car()
print(car.start())
3Inheritance
Inheritance allows a class to inherit attributes and methods from another class, promoting code reuse.
Key Points:
- Represents "IS-A" relationship between classes
- Child class inherits properties and behaviors from parent class
- Promotes code reusability and reduces redundancy
- Supports hierarchical classification
- Enables method overriding for specialized behavior
- Parent class is also called base/super class, child is derived/sub class
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
return f"{self.name} makes a sound"
class Dog(Animal):
def speak(self):
return f"{self.name} barks"
class Cat(Animal):
def speak(self):
return f"{self.name} meows"
dog = Dog("Buddy")
cat = Cat("Whiskers")
print(dog.speak())
print(cat.speak())
Whiskers meows
4Polymorphism
Polymorphism allows objects of different classes to be treated as objects of a common base class, with each implementing methods differently.
Key Points:
- Means "many forms" - one interface, multiple implementations
- Achieved through method overriding and method overloading
- Enables writing flexible and reusable code
- Two types: compile-time (method overloading) and runtime (method overriding)
- Allows different objects to respond to same method call differently
- Supports dynamic method resolution
class Shape:
def area(self):
pass
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return 3.14 * self.radius ** 2
shapes = [Rectangle(5, 4), Circle(3)]
for shape in shapes:
print(f"Area: {shape.area()}")
Area: 28.26
5Composition
Composition is a design principle where a class contains instances of other classes, representing a "has-a" relationship.
Key Points:
- Represents "HAS-A" relationship between objects
- More flexible than inheritance in many scenarios
- Enables building complex objects from simpler ones
- Follows the principle "favor composition over inheritance"
- Composed objects can be reused independently
- Reduces coupling and increases modularity
class Engine:
def start(self):
return "Engine started"
class Car:
def __init__(self):
self.engine = Engine() # Composition
def start(self):
return self.engine.start()
car = Car()
print(car.start())
6Method Overloading
Python doesn't support traditional method overloading, but we can achieve similar functionality using default arguments or variable arguments.
Key Points:
- Allows multiple methods with same name but different parameters
- Python doesn't support it natively like Java or C++
- Can be simulated using default arguments or *args, **kwargs
- Latest definition overwrites previous ones in Python
- Provides flexibility in method calling
- Compile-time polymorphism concept
class Calculator:
def add(self, a, b=0, c=0):
return a + b + c
calc = Calculator()
print(calc.add(5))
print(calc.add(5, 10))
print(calc.add(5, 10, 15))
15
30
7Method Overriding
Method overriding allows a subclass to provide a specific implementation of a method already defined in its parent class.
Key Points:
- Child class redefines parent class method with same name
- Enables runtime polymorphism
- Method signature must match the parent method
- Use super() to call parent class method
- Provides specialized behavior for child classes
- Essential for implementing polymorphic behavior
class Parent:
def show(self):
return "Parent method"
class Child(Parent):
def show(self):
return "Child method"
child = Child()
print(child.show())
8Multiple Inheritance
A class can inherit from multiple parent classes, gaining attributes and methods from all of them.
Key Points:
- Python supports multiple inheritance unlike Java
- Child class inherits from two or more parent classes
- Uses Method Resolution Order (MRO) to resolve conflicts
- MRO follows C3 linearization algorithm
- Can lead to diamond problem if not carefully designed
- Use mro() method to check resolution order
class Flyer:
def fly(self):
return "Flying in the sky"
class Swimmer:
def swim(self):
return "Swimming in water"
class Duck(Flyer, Swimmer):
pass
duck = Duck()
print(duck.fly())
print(duck.swim())
Swimming in water
9Private Members
Private members (attributes/methods) are prefixed with double underscore (__) and are not directly accessible outside the class.
Key Points:
- Prefix with __ (double underscore) to make members private
- Implements name mangling for privacy
- Not truly private but harder to access from outside
- Single underscore (_) indicates protected members by convention
- Accessible within class using _ClassName__member
- Helps prevent accidental modification of internal state
class SecureData:
def __init__(self):
self.__private_data = "Secret"
self.public_data = "Public"
def __private_method(self):
return "Private method"
def access_private(self):
return self.__private_method()
obj = SecureData()
print(obj.public_data)
print(obj.access_private())
Private method
10Class Variables
Class variables are shared among all instances of a class and defined within the class but outside any methods.
Key Points:
- Shared across all instances of the class
- Defined directly inside class body, outside methods
- Accessed using ClassName.variable or instance.variable
- Changing via class name affects all instances
- Useful for constants and shared configuration
- Occupies single memory location
class Employee:
company = "TechCorp" # Class variable
def __init__(self, name):
self.name = name
emp1 = Employee("Alice")
emp2 = Employee("Bob")
print(emp1.company)
print(emp2.company)
Employee.company = "NewCorp"
print(emp1.company)
TechCorp
NewCorp
11Instance Variables
Instance variables are unique to each instance and are typically defined in the __init__ method.
Key Points:
- Unique to each object instance
- Defined inside __init__() method using self
- Each instance has its own copy in memory
- Can have different values for different objects
- Accessed using self.variable within class
- Represent object's state and attributes
class Person:
def __init__(self, name, age):
self.name = name # Instance variable
self.age = age # Instance variable
person1 = Person("Alice", 30)
person2 = Person("Bob", 25)
print(f"{person1.name} is {person1.age}")
print(f"{person2.name} is {person2.age}")
Bob is 25
12Abstract Classes
Abstract classes cannot be instantiated and require subclasses to implement abstract methods.
Key Points:
- Cannot create objects directly from abstract classes
- Used as blueprint for other classes
- Contains one or more abstract methods
- Implemented using ABC module and @abstractmethod decorator
- Forces subclasses to implement specific methods
- Provides common interface for group of related classes
from abc import ABC, abstractmethod
class PaymentMethod(ABC):
@abstractmethod
def pay(self, amount):
pass
class CreditCard(PaymentMethod):
def pay(self, amount):
return f"Paid ${amount} using Credit Card"
payment = CreditCard()
print(payment.pay(100))
13Static Methods
Static methods don't access instance or class state and are defined using @staticmethod decorator.
Key Points:
- Don't receive self or cls as first parameter
- Cannot access instance or class variables
- Defined using @staticmethod decorator
- Called using ClassName.method() or instance.method()
- Used for utility functions related to class
- Behaves like regular function but belongs to class namespace
class MathOperations:
@staticmethod
def add(a, b):
return a + b
@staticmethod
def multiply(a, b):
return a * b
print(MathOperations.add(5, 3))
print(MathOperations.multiply(4, 2))
8
14Class Methods
Class methods receive the class as the first argument and are defined using @classmethod decorator.
Key Points:
- Takes cls (class) as first parameter instead of self
- Can access and modify class state
- Defined using @classmethod decorator
- Often used for factory methods (alternative constructors)
- Can be called on class or instance
- Works with class variables, not instance variables
class Date:
def __init__(self, year, month, day):
self.year = year
self.month = month
self.day = day
@classmethod
def from_string(cls, date_string):
year, month, day = map(int, date_string.split('-'))
return cls(year, month, day)
date = Date.from_string("2024-03-15")
print(f"{date.year}-{date.month}-{date.day}")
15Decorators
Decorators are functions that modify the behavior of other functions or methods.
Key Points:
- Functions that take another function as argument
- Add functionality without modifying original function
- Applied using @decorator_name syntax
- Follow the wrapper pattern
- Can be stacked (multiple decorators on one function)
- Common examples: @staticmethod, @classmethod, @property
def uppercase_decorator(func):
def wrapper():
result = func()
return result.upper()
return wrapper
@uppercase_decorator
def greet():
return "hello world"
print(greet())
16Dunder Methods
Dunder (double underscore) methods are special methods that enable operator overloading and custom behavior.
Key Points:
- Also called magic methods or special methods
- Surrounded by double underscores (e.g., __init__, __str__)
- Invoked automatically by Python interpreter
- Enable operator overloading and custom behavior
- Examples: __init__, __str__, __len__, __add__, __eq__
- Make classes behave like built-in types
class Book:
def __init__(self, title, pages):
self.title = title
self.pages = pages
def __str__(self):
return f"Book: {self.title}"
def __len__(self):
return self.pages
book = Book("Python Guide", 350)
print(book)
print(f"Pages: {len(book)}")
Pages: 350
17Operator Overloading
Operator overloading allows you to define custom behavior for operators like +, -, *, etc.
Key Points:
- Gives extended meaning to built-in operators
- Implemented using dunder methods
- Makes custom objects work with standard operators
- Examples: __add__ for +, __sub__ for -, __mul__ for *
- Improves code readability and intuitiveness
- Comparison operators: __eq__, __lt__, __gt__, __le__, __ge__
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
return Vector(self.x + other.x, self.y + other.y)
def __str__(self):
return f"Vector({self.x}, {self.y})"
v1 = Vector(2, 3)
v2 = Vector(4, 5)
v3 = v1 + v2
print(v3)
18Property Decorators
Property decorators allow you to define getter, setter, and deleter methods for attributes.
Key Points:
- Makes methods accessible like attributes
- Provides controlled access to private attributes
- Uses @property for getter, @name.setter for setter
- Enables data validation and computed properties
- Maintains encapsulation while providing attribute syntax
- Can add logic when getting or setting values
class Temperature:
def __init__(self, celsius):
self._celsius = celsius
@property
def fahrenheit(self):
return (self._celsius * 9/5) + 32
@fahrenheit.setter
def fahrenheit(self, value):
self._celsius = (value - 32) * 5/9
temp = Temperature(25)
print(f"{temp.fahrenheit}°F")
temp.fahrenheit = 86
print(f"{temp._celsius}°C")
30.0°C
19Duck Typing
Duck typing focuses on what an object can do rather than what it is. "If it walks like a duck and quacks like a duck, it's a duck."
Key Points:
- Type is determined by behavior, not inheritance
- Objects defined by what they can do, not what they are
- Fundamental to Python's dynamic typing system
- Promotes flexibility and code reusability
- No need for explicit type declarations
- Focuses on interfaces rather than types
class Duck:
def quack(self):
return "Quack!"
class Person:
def quack(self):
return "I'm imitating a duck!"
def make_it_quack(thing):
print(thing.quack())
duck = Duck()
person = Person()
make_it_quack(duck)
make_it_quack(person)
I'm imitating a duck!
20Mixin Classes
Mixins are classes that provide methods to other classes through multiple inheritance but aren't meant to stand alone.
Key Points:
- Small classes designed to add specific functionality
- Not intended to be instantiated independently
- Used through multiple inheritance
- Promotes code reuse without deep inheritance hierarchies
- Typically named with "Mixin" suffix by convention
- Alternative to deep inheritance chains
class JSONMixin:
def to_json(self):
import json
return json.dumps(self.__dict__)